/*
所谓watch， 就是观察一个响应式数据， 当数据发生变化时， 执行响应的回调函数
*/
const bucket = new WeakMap()

let activeEffect
const effectStack = []
function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn)
    const res = fn()
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
    return res
  }
  effectFn.options = options
  effectFn.deps = []
  // 新增， 如果不是lazy，就立刻执行
  if (!options.lazy) {
    effectFn()
  }
  return effectFn
}

function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  effectFn.deps.length = 0
}

function track(target, key) {
  if (!activeEffect) return
  let depsMap = bucket.get(target)
  if (!depsMap) bucket.set(target, (depsMap = new Map()))
  let deps = depsMap.get(key)
  if (!deps) depsMap.set(key, (deps = new Set()))
  deps.add(activeEffect)
  activeEffect.deps.push(deps)
}

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const deps = depsMap.get(key)
  if (!deps) return
  const effectsToRun = new Set()
  deps.forEach((effectFn) => {
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })
  effectsToRun.forEach((effectFn) => {
    // 调度器使用
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      effectFn()
    }
  })
}

/* // 实现计算属性
function computed(getter) {
  let val
  let dirty = true // 脏值标记，如果dirty则执行effectFn， 否则返回缓存val
  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      dirty = true // 调度器执行时，将dirty改为true
      trigger(res, 'value') // 在effect中读取计算属性的value，需要手动trigger effectFn的执行
    },
  })

  const res = {
    get value() {
      if (dirty) {
        val = effectFn()
        dirty = false
      }
      // 计算属性，需要手动track
      track(res, 'value')
      return val
    },
  }
  return res
} */

// 数据源
const data = { foo: 'hello', bar: 1 }

const obj = new Proxy(data, {
  get(target, key) {
    track(target, key)
    return Reflect.get(target, key)
  },
  set(target, key, val) {
    Reflect.set(target, key, val)
    trigger(target, key)
    return true
  },
})

/**
 * watch接收一个响应式数据(或者一个函数返回响应式数据)和一个回调函数
 * @param {Object|Function} source
 * @param {Function} cb
 * @param {{immediate:boolean, flush:'pre'|'post'}} options
 */
function watch(source, cb, options = {}) {
  let getter
  let oldVal, newVal
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  const job = () => {
    newVal = effectFn()
    cb(newVal, oldVal)
    oldVal = newVal
  }
  const effectFn = effect(
    () => {
      // 要触发source的读取操作
      // return source.foo // 这么写硬编码了，要对source所有的属性track一次， 即触发属性的读取操作
      // traverse(source) // 深度触发get 执行track, 还是有一个小问题，vue3支持source为一个返回响应式数据的函数
      return getter()
    },
    {
      lazy: true, // 使用lazy， 将effectFn缓存
      // 调度器
      /* scheduler() {
        // 在source变化 触发effectFn执行时，执行cb
        // cb()  // 这么写还差点意思，因为vue的watch可以拿到newVal和oldVal
      }, */
      scheduler: () => {
        if (options.flush === 'post') {
          // flush是post则放到微任务队列中执行
          const p = Promise.resolve()
          p.then(job)
        } else {
          job()
        }
      },
    }
  )
  if (options.immediate) {
    job()
  } else {
    oldVal = effectFn() // 获取一次oldVal
  }
}

/**
 * 遍历value，触发属性的get操作
 * @param {*} value
 * @param {*} seen 防止循环引用导致的无限递归
 */
function traverse(value, seen = new Set()) {
  // 原始值 空值 已get过的值
  if (typeof value !== 'object' || value == null || seen.has(value)) return
  seen.add(value)
  for (const key of Object.keys(value)) {
    traverse(value[key])
  }
  return value // 触发get操作
}

// =====================测试==========================
/* watch(obj, (newVal, oldVal) => {
  console.log('watch changed', newVal, oldVal)
})

obj.foo += ' world'
obj.bar++ */

// ===================测试immediate====================
/* watch(
  () => obj.foo,
  (newVal, oldVal) => {
    console.log('immediate watch changed', newVal, oldVal)
  },
  {
    immediate: true, // watch创建的时候就会执行回调
  }
)
obj.foo += ' world' */

// =================测试flush==================
/*
flush是vue3的watch新参数, 用于控制回调执行的时机
分为 pre, post, sync(很少用)
pre是默认的，如果此时有修改DOM或者模板相关的操作，会在回调执行后才执行操作，即此时拿到的是旧的模板
post表示模板更新完成后执行回调，此时this.$refs这样拿到的都是最新的数据
sync
*/
watch(
  () => obj.foo,
  (newVal, oldVal) => {
    console.log('immediate watch changed', newVal, oldVal)
  },
  {
    flush: 'post',
  }
)
obj.foo += ' world'
